import { xmlDecode, isDateFmt, dateToExcel, excelToDate } from '../../../utils/utils';
import BaseXform from '../base-xform';
import Range from '../../../doc/range';
import Enums from '../../../doc/enums';

import RichTextXform from '../strings/rich-text-xform';

function getValueType(v) {
    if (v === null || v === undefined) {
        return Enums.ValueType.Null;
    }
    if (v instanceof String || typeof v === 'string') {
        return Enums.ValueType.String;
    }
    if (typeof v === 'number') {
        return Enums.ValueType.Number;
    }
    if (typeof v === 'boolean') {
        return Enums.ValueType.Boolean;
    }
    if (v instanceof Date) {
        return Enums.ValueType.Date;
    }
    if (v.text && v.hyperlink) {
        return Enums.ValueType.Hyperlink;
    }
    if (v.formula) {
        return Enums.ValueType.Formula;
    }
    if (v.error) {
        return Enums.ValueType.Error;
    }
    throw new Error('I could not understand type of value');
}

function getEffectiveCellType(cell) {
    switch (cell.type) {
        case Enums.ValueType.Formula:
            return getValueType(cell.result);
        default:
            return cell.type;
    }
}

class CellXform extends BaseXform {
    constructor() {
        super();

        this.richTextXForm = new RichTextXform();
    }

    get tag() {
        return 'c';
    }

    prepare(model, options) {
        const styleId = options.styles.addStyleModel(model.style || {}, getEffectiveCellType(model));
        if (styleId) {
            model.styleId = styleId;
        }

        if (model.comment) {
            options.comments.push({ ...model.comment, ref: model.address });
        }

        switch (model.type) {
            case Enums.ValueType.String:
            case Enums.ValueType.RichText:
                if (options.sharedStrings) {
                    model.ssId = options.sharedStrings.add(model.value);
                }
                break;

            case Enums.ValueType.Date:
                if (options.date1904) {
                    model.date1904 = true;
                }
                break;

            case Enums.ValueType.Hyperlink:
                if (options.sharedStrings && model.text !== undefined && model.text !== null) {
                    model.ssId = options.sharedStrings.add(model.text);
                }
                options.hyperlinks.push({
                    address: model.address,
                    target: model.hyperlink,
                    tooltip: model.tooltip,
                });
                break;

            case Enums.ValueType.Merge:
                options.merges.add(model);
                break;

            case Enums.ValueType.Formula:
                if (options.date1904) {
                    // in case valueType is date
                    model.date1904 = true;
                }

                if (model.shareType === 'shared') {
                    model.si = options.siFormulae++;
                }

                if (model.formula) {
                    options.formulae[model.address] = model;
                } else if (model.sharedFormula) {
                    const master = options.formulae[model.sharedFormula];
                    if (!master) {
                        throw new Error(
                            `Shared Formula master must exist above and or left of clone for cell ${model.address}`
                        );
                    }
                    if (master.si === undefined) {
                        master.shareType = 'shared';
                        master.si = options.siFormulae++;
                        master.range = new Range(master.address, model.address);
                    } else if (master.range) {
                        master.range.expandToAddress(model.address);
                    }
                    model.si = master.si;
                }
                break;

            default:
                break;
        }
    }

    renderFormula(xmlStream, model) {
        let attrs = null;
        switch (model.shareType) {
            case 'shared':
                attrs = {
                    t: 'shared',
                    ref: model.ref || model.range.range,
                    si: model.si,
                };
                break;

            case 'array':
                attrs = {
                    t: 'array',
                    ref: model.ref,
                };
                break;

            default:
                if (model.si !== undefined) {
                    attrs = {
                        t: 'shared',
                        si: model.si,
                    };
                }
                break;
        }

        switch (getValueType(model.result)) {
            case Enums.ValueType.Null: // ?
                xmlStream.leafNode('f', attrs, model.formula);
                break;

            case Enums.ValueType.String:
                // oddly, formula results don't ever use shared strings
                xmlStream.addAttribute('t', 'str');
                xmlStream.leafNode('f', attrs, model.formula);
                xmlStream.leafNode('v', null, model.result);
                break;

            case Enums.ValueType.Number:
                xmlStream.leafNode('f', attrs, model.formula);
                xmlStream.leafNode('v', null, model.result);
                break;

            case Enums.ValueType.Boolean:
                xmlStream.addAttribute('t', 'b');
                xmlStream.leafNode('f', attrs, model.formula);
                xmlStream.leafNode('v', null, model.result ? 1 : 0);
                break;

            case Enums.ValueType.Error:
                xmlStream.addAttribute('t', 'e');
                xmlStream.leafNode('f', attrs, model.formula);
                xmlStream.leafNode('v', null, model.result.error);
                break;

            case Enums.ValueType.Date:
                xmlStream.leafNode('f', attrs, model.formula);
                xmlStream.leafNode('v', null, dateToExcel(model.result, model.date1904));
                break;

            // case Enums.ValueType.Hyperlink: // ??
            // case Enums.ValueType.Formula:
            default:
                throw new Error('I could not understand type of value');
        }
    }

    render(xmlStream, model) {
        if (model.type === Enums.ValueType.Null && !model.styleId) {
            // if null and no style, exit
            return;
        }

        xmlStream.openNode('c');
        xmlStream.addAttribute('r', model.address);

        if (model.styleId) {
            xmlStream.addAttribute('s', model.styleId);
        }

        switch (model.type) {
            case Enums.ValueType.Null:
                break;

            case Enums.ValueType.Number:
                xmlStream.leafNode('v', null, model.value);
                break;

            case Enums.ValueType.Boolean:
                xmlStream.addAttribute('t', 'b');
                xmlStream.leafNode('v', null, model.value ? '1' : '0');
                break;

            case Enums.ValueType.Error:
                xmlStream.addAttribute('t', 'e');
                xmlStream.leafNode('v', null, model.value.error);
                break;

            case Enums.ValueType.String:
            case Enums.ValueType.RichText:
                if (model.ssId !== undefined) {
                    xmlStream.addAttribute('t', 's');
                    xmlStream.leafNode('v', null, model.ssId);
                } else if (model.value && model.value.richText) {
                    xmlStream.addAttribute('t', 'inlineStr');
                    xmlStream.openNode('is');
                    model.value.richText.forEach(text => {
                        this.richTextXForm.render(xmlStream, text);
                    });
                    xmlStream.closeNode('is');
                } else {
                    xmlStream.addAttribute('t', 'str');
                    xmlStream.leafNode('v', null, model.value);
                }
                break;

            case Enums.ValueType.Date:
                xmlStream.leafNode('v', null, dateToExcel(model.value, model.date1904));
                break;

            case Enums.ValueType.Hyperlink:
                if (model.ssId !== undefined) {
                    xmlStream.addAttribute('t', 's');
                    xmlStream.leafNode('v', null, model.ssId);
                } else {
                    xmlStream.addAttribute('t', 'str');
                    xmlStream.leafNode('v', null, model.text);
                }
                break;

            case Enums.ValueType.Formula:
                this.renderFormula(xmlStream, model);
                break;

            case Enums.ValueType.Merge:
                // nothing to add
                break;

            default:
                break;
        }

        xmlStream.closeNode(); // </c>
    }


    parseXmlObject(node) {
        if (node._name === 'c') {
            this.model = {
                address: node._attributes.r,
            };
            this.t = node._attributes.t;
            if (node._attributes.s) {
                this.model.styleId = parseInt(node._attributes.s, 10);
            }
            node._elements?.forEach(enode => {
                switch (enode._name) {
                    case 'f':
                        this.model.si = enode._attributes?.si;
                        this.model.shareType = enode._attributes?.t;
                        this.model.ref = enode._attributes?.ref;
                        let ftext = ""
                        enode._elements?.forEach((item) => {
                            ftext = item._text
                        })
                        this.model.formula = this.model.formula ? this.model.formula + ftext : ftext;
                        break;

                    case 'v':
                        let vtext = ""
                        enode._elements?.forEach((item) => {
                            vtext = item._text
                        })
                        if (this.model.value && this.model.value.richText) {
                            this.model.value.richText.text = this.model.value.richText.text
                                ? this.model.value.richText.text + vtext
                                : vtext;
                        } else {
                            this.model.value = this.model.value ? this.model.value + vtext : vtext;
                        }
                        break;

                    case 't':
                        let ttext = ""
                        enode._elements?.forEach((item) => {
                            ttext = item._text
                        })
                        if (this.model.value && this.model.value.richText) {
                            this.model.value.richText.text = this.model.value.richText.text
                                ? this.model.value.richText.text + ttext
                                : ttext;
                        } else {
                            this.model.value = this.model.value ? this.model.value + ttext : ttext;
                        }
                        break;

                    case 'r':
                        this.parser = this.richTextXForm;
                        this.parser.parseXmlObject(enode);
                        this.model.value = this.model.value || {};
                        this.model.value.richText = this.model.value.richText || [];
                        this.model.value.richText.push(this.parser.model);
                        this.parser = undefined;
                        break;

                    default:
                        break;
                }

            })

            // first guess on cell type
            if (this.model.formula || this.model.shareType) {
                this.model.type = Enums.ValueType.Formula;
                if (this.model.value) {
                    if (this.t === 'str') {
                        this.model.result = xmlDecode(this.model.value);
                    } else if (this.t === 'b') {
                        this.model.result = parseInt(this.model.value, 10) !== 0;
                    } else if (this.t === 'e') {
                        this.model.result = { error: this.model.value };
                    } else {
                        this.model.result = parseFloat(this.model.value);
                    }
                    this.model.value = undefined;
                }
            } else if (this.model.value !== undefined) {
                switch (this.t) {
                    case 's':
                        this.model.type = Enums.ValueType.String;
                        this.model.value = parseInt(this.model.value, 10);
                        break;
                    case 'str':
                        this.model.type = Enums.ValueType.String;
                        this.model.value = xmlDecode(this.model.value);
                        break;
                    case 'inlineStr':
                        this.model.type = Enums.ValueType.String;
                        break;
                    case 'b':
                        this.model.type = Enums.ValueType.Boolean;
                        this.model.value = parseInt(this.model.value, 10) !== 0;
                        break;
                    case 'e':
                        this.model.type = Enums.ValueType.Error;
                        this.model.value = { error: this.model.value };
                        break;
                    default:
                        this.model.type = Enums.ValueType.Number;
                        this.model.value = parseFloat(this.model.value);
                        break;
                }
            } else if (this.model.styleId) {
                this.model.type = Enums.ValueType.Null;
            } else {
                this.model.type = Enums.ValueType.Merge;
            }
            return true;
        }
        return false;
    }

    parseOpen(node) {
        if (this.parser) {
            this.parser.parseOpen(node);
            return true;
        }
        switch (node.name) {
            case 'c':
                this.model = {
                    address: node.attributes.r,
                };
                this.t = node.attributes.t;
                if (node.attributes.s) {
                    this.model.styleId = parseInt(node.attributes.s, 10);
                }
                return true;

            case 'f':
                this.currentNode = 'f';
                this.model.si = node.attributes.si;
                this.model.shareType = node.attributes.t;
                this.model.ref = node.attributes.ref;
                return true;

            case 'v':
                this.currentNode = 'v';
                return true;

            case 't':
                this.currentNode = 't';
                return true;

            case 'r':
                this.parser = this.richTextXForm;
                this.parser.parseOpen(node);
                return true;

            default:
                return false;
        }
    }

    parseText(text) {
        if (this.parser) {
            this.parser.parseText(text);
            return;
        }
        switch (this.currentNode) {
            case 'f':
                this.model.formula = this.model.formula ? this.model.formula + text : text;
                break;
            case 'v':
            case 't':
                if (this.model.value && this.model.value.richText) {
                    this.model.value.richText.text = this.model.value.richText.text
                        ? this.model.value.richText.text + text
                        : text;
                } else {
                    this.model.value = this.model.value ? this.model.value + text : text;
                }
                break;
            default:
                break;
        }
    }

    parseClose(name) {
        switch (name) {
            case 'c': {
                const { model } = this;

                // first guess on cell type
                if (model.formula || model.shareType) {
                    model.type = Enums.ValueType.Formula;
                    if (model.value) {
                        if (this.t === 'str') {
                            model.result = xmlDecode(model.value);
                        } else if (this.t === 'b') {
                            model.result = parseInt(model.value, 10) !== 0;
                        } else if (this.t === 'e') {
                            model.result = { error: model.value };
                        } else {
                            model.result = parseFloat(model.value);
                        }
                        model.value = undefined;
                    }
                } else if (model.value !== undefined) {
                    switch (this.t) {
                        case 's':
                            model.type = Enums.ValueType.String;
                            model.value = parseInt(model.value, 10);
                            break;
                        case 'str':
                            model.type = Enums.ValueType.String;
                            model.value = xmlDecode(model.value);
                            break;
                        case 'inlineStr':
                            model.type = Enums.ValueType.String;
                            break;
                        case 'b':
                            model.type = Enums.ValueType.Boolean;
                            model.value = parseInt(model.value, 10) !== 0;
                            break;
                        case 'e':
                            model.type = Enums.ValueType.Error;
                            model.value = { error: model.value };
                            break;
                        default:
                            model.type = Enums.ValueType.Number;
                            model.value = parseFloat(model.value);
                            break;
                    }
                } else if (model.styleId) {
                    model.type = Enums.ValueType.Null;
                } else {
                    model.type = Enums.ValueType.Merge;
                }
                return false;
            }

            case 'f':
            case 'v':
            case 'is':
                this.currentNode = undefined;
                return true;

            case 't':
                if (this.parser) {
                    this.parser.parseClose(name);
                    return true;
                }
                this.currentNode = undefined;
                return true;

            case 'r':
                this.model.value = this.model.value || {};
                this.model.value.richText = this.model.value.richText || [];
                this.model.value.richText.push(this.parser.model);
                this.parser = undefined;
                this.currentNode = undefined;
                return true;

            default:
                if (this.parser) {
                    this.parser.parseClose(name);
                    return true;
                }
                return false;
        }
    }

    reconcile(model, options) {
        const style = model.styleId && options.styles && options.styles.getStyleModel(model.styleId);
        if (style) {
            model.style = style;
        }
        if (model.styleId !== undefined) {
            model.styleId = undefined;
        }

        switch (model.type) {
            case Enums.ValueType.String:
                if (typeof model.value === 'number') {
                    if (options.sharedStrings) {
                        model.value = options.sharedStrings.getString(model.value);
                    }
                }
                if (model.value.richText) {
                    model.type = Enums.ValueType.RichText;
                }
                break;

            case Enums.ValueType.Number:
                if (style && isDateFmt(style.numFmt)) {
                    model.type = Enums.ValueType.Date;
                    model.value = excelToDate(model.value, options.date1904);
                }
                break;

            case Enums.ValueType.Formula:
                if (model.result !== undefined && style && isDateFmt(style.numFmt)) {
                    model.result = excelToDate(model.result, options.date1904);
                }
                if (model.shareType === 'shared') {
                    if (model.ref) {
                        // master
                        options.formulae[model.si] = model.address;
                    } else {
                        // slave
                        model.sharedFormula = options.formulae[model.si];
                        delete model.shareType;
                    }
                    delete model.si;
                }
                break;

            default:
                break;
        }

        // look for hyperlink
        const hyperlink = options.hyperlinkMap[model.address];
        if (hyperlink) {
            if (model.type === Enums.ValueType.Formula) {
                model.text = model.result;
                model.result = undefined;
            } else {
                model.text = model.value;
                model.value = undefined;
            }
            model.type = Enums.ValueType.Hyperlink;
            model.hyperlink = hyperlink;
        }

        const comment = options.commentsMap && options.commentsMap[model.address];
        if (comment) {
            model.comment = comment;
        }
    }
}

export default CellXform;
